<?php
declare(strict_types=1);

namespace TairClient\Client;

use Hyperf\GrpcServer\Exception\GrpcException;
use Hyperf\Redis\Redis;
use Hyperf\Utils\Codec\Json;
use Hyperfx\Framework\Logger\Logx;

class TairDoc extends TairBase {

    public function __construct(protected Redis $client)
    {

    }

    /**
     * 创建key并将JSON的值存储在对应的path中，若key及目标path已经存在，则更新对应的JSON值。
     *
     * @link https://help.aliyun.com/document_detail/441558.html#section-aph-u42-8qg
     *
     * @var string $key TairDoc的key，用于指定作为命令调用对象的TairDoc。
     * @var string $path 目标key的path。
     * @var mixed $data 待新增或更新的JSON数据。
     * @var string $limit NX：当path不存在时写入,  XX：当path存在时写入。
     * @return bool
     */
    public function set(string $key, string $path, $data, string $limit = '', bool $falseIsError = true): bool {
        $this->checkNotEmpty($key);
        $this->checkNotEmpty($path);
        $dataString = is_array($data) || is_object($data) ? Json::encode($data) : $data;

        return $this->then(function (string $key, string $path, $dataString, string $limit) {
            if (empty($limit)) {
                return $this->client->rawCommand('JSON.SET', $key, $path, $dataString);
            }
            return $this->client->rawCommand('JSON.SET', $key, $path, $dataString, $limit);
        }, [$key, $path, $dataString, $limit], function () use ($key, $path, $dataString, $limit) {
            return sprintf('JSON.SET %s %s %s %s', $key, $path, $dataString, $limit);
        }, $falseIsError);
    }

    /**
     * 获取目标key、path中存储的JSON数据。
     *
     * @link https://help.aliyun.com/document_detail/441558.html#section-y0o-dkv-xzy
     *
     * @param string $key TairDoc的key，用于指定作为命令调用对象的TairDoc。
     * @param string $path 目标key的path，支持JSONPath与JSON语法，按需灵活地查询，更多信息请参见JSONPath介绍和JSONPointer介绍。 https://help.aliyun.com/document_detail/441558.html#section-oaj-cgf-v0k
     * @return array 返回的数据
     */
    public function get(string $key, string $path, string $returnType = 'array'): array|bool|string {
        $this->checkNotEmpty($key);
        $this->checkNotEmpty($path);

        $data = $this->then(function (string $key, string $path) {
            return $this->client->rawCommand('JSON.GET', $key, $path);
        }, [$key, $path], function () use ($key, $path) {
            return sprintf('JSON.GET %s %s', $key, $path);
        }, false);

        if (false === $data) {
            return false;
        }

        if (empty($data)) {
            return [];
        }
        if (0 === strcmp($returnType, 'array')) {
            return Json::decode($data);
        }
        return $data;
    }

    /**
     * 获取目标key、path中存储的JSON数据。
     *
     * @link https://help.aliyun.com/document_detail/441558.html#section-y0o-dkv-xzy
     *
     * @param string $key TairDoc的key，用于指定作为命令调用对象的TairDoc。
     * @param string $path 目标key的path，支持JSONPath与JSON语法，按需灵活地查询，更多信息请参见JSONPath介绍和JSONPointer介绍。 https://help.aliyun.com/document_detail/441558.html#section-oaj-cgf-v0k
     * @return int 成功数量
     */
    public function del(string $key, string $path): int {
        $this->checkNotEmpty($key);
        $this->checkNotEmpty($path);

        return $this->then(function (string $key, string $path) {
            return $this->client->rawCommand('JSON.DEL', $key, $path);
        }, [$key, $path], function () use ($key, $path) {
            return sprintf('JSON.DEL %s %s', $key, $path);
        });
    }

    /**
     * 获取目标key中path对应值的类型，结果可能包括boolean、string、number、array、object、raw、reference、const、null等。
     *
     * @link https://help.aliyun.com/document_detail/441558.htm?spm=a2c4g.11186623.0.0.5af048d1dIr5Yq#section-ms4-hvy-921
     *
     * @param string $key TairDoc的key，用于指定作为命令调用对象的TairDoc。
     * @param string $path 目标key的path
     * @return string 返回类型
     */
    public function type(string $key, string $path): string {
        $this->checkNotEmpty($key);
        $this->checkNotEmpty($path);

        return $this->then(function (string $key, string $path) {
            return $this->client->rawCommand('JSON.TYPE', $key, $path);
        }, [$key, $path], function () use ($key, $path) {
            return sprintf('JSON.TYPE %s %s', $key, $path);
        });
    }

    /**
     * 对目标key中path对应的值增加value，path对应的值和待增加的value必须是int或double类型。
     *
     * @link https://help.aliyun.com/document_detail/441558.html#section-bwp-eq0-wc6
     *
     * @var string $key TairDoc的key，用于指定作为命令调用对象的TairDoc。
     * @var string $path 目标key的path。
     * @var int $value 待增加的数值
     * @return bool
     */
    public function numIncrby(string $key, string $path, int $value): int|bool {
        $this->checkNotEmpty($key);
        $this->checkNotEmpty($path);

        $args = [$key, $path, $value];

        $ret = $this->then(function ($args) {
            return $this->client->rawCommand('JSON.NUMINCRBY', ...$args);
        }, [$args], function () use ($args) {
            return sprintf('JSON.NUMINCRBY %s', implode(' ', $args));
        }, false);
        if (is_string($ret)) {
            return (int) $ret;
        }
        return $ret;
    }

    /**
     * 移除并返回path对应数组（array）中指定位置（index）的元素。
     *
     * @param string $key TairDoc的key，用于指定作为命令调用对象的TairDoc。
     * @param string $path 目标key的path
     * @param int $index 数组的索引，起始下标为0，负数表示反向取值，若不传该参数默认为最后一个元素。
     * @return string|array|int 执行成功：移除并返回该元素。
     */
    public function arrPop(string $key, string $path, int $index) {
        $this->checkNotEmpty($key);
        $this->checkNotEmpty($path);

        return $this->then(function (string $key, string $path, $index) {
            return $this->client->rawCommand('JSON.ARRPOP', $key, $path, $index);
        }, [$key, $path, $index], function () use ($key, $path, $index) {
            return sprintf('JSON.ARRPOP %s %s %u', $key, $path, $index);
        }, false);
    }

    /**
     * 在指定path对应数组（array）的末尾添加JSON数据，支持添加多个JSON。
     *
     * @param string $key TairDoc的key，用于指定作为命令调用对象的TairDoc。
     * @param string $path 目标key的path
     * @param string|int $json 需要插入的数据。
     * @return int 返回操作完成后数组（array）中的元素数量。
     */
    public function arrAppend(string $key, string $path, $json): int {
        $this->checkNotEmpty($key);
        $this->checkNotEmpty($path);
        $this->checkNotEmpty($json);
        foreach ($json as &$item) {
            if (is_array($item)) {
                $item = Json::encode($item);
            }
        }
        return $this->then(function (string $key, string $path, $json) {
            return $this->client->rawCommand('JSON.ARRAPPEND', $key, $path, ...$json);
        }, [$key, $path, $json], function () use ($key, $path, $json) {
            return sprintf('JSON.ARRAPPEND %s %s', $key, $path);
        });
    }

    /**
     * 将JSON插入到path对应的数组（array）中，原有元素会往后移动。
     *
     * @param string $key TairDoc的key，用于指定作为命令调用对象的TairDoc。
     * @param string $path 目标key的path
     * @param int $index 数组的索引，起始下标为0，负数表示反向取值，若不传该参数默认为最后一个元素。
     * @param string|int $json 需要插入的数据。
     * @return int 返回操作完成后数组（array）中的元素数量。
     */
    public function arrInsert(string $key, string $path, $json): int {
        $this->checkNotEmpty($key);
        $this->checkNotEmpty($path);
        $this->checkNotEmpty($json);
        foreach ($json as &$item) {
            if (is_array($item)) {
                $item = Json::encode($item);
            }
        }
        return $this->then(function (string $key, string $path, $json) {
            return $this->client->rawCommand('JSON.ARRINSERT', $key, $path, ...$json);
        }, [$key, $path, $json], function () use ($key, $path, $json) {
            return sprintf('JSON.ARRINSERT %s %s', $key, $path);
        });
    }

    /**
     * 获取path对应数组（array）的长度
     *
     * @param string $key TairDoc的key，用于指定作为命令调用对象的TairDoc。
     * @param string $path 目标key的path
     * @return int 数组（array）的长度
     */
    public function arrLen(string $key, string $path): int {
        $this->checkNotEmpty($key);
        $this->checkNotEmpty($path);

        return $this->then(function (string $key, string $path) {
            return $this->client->rawCommand('JSON.ARRLEN', $key, $path);
        }, [$key, $path], function () use ($key, $path) {
            return sprintf('JSON.ARRLEN %s %s', $key, $path);
        });
    }

    /**
     * 修剪目标key的path对应的数组（array），保留start至stop范围内的数据。
     *
     * @param string $key TairDoc的key，用于指定作为命令调用对象的TairDoc。
     * @param string $path 目标key的path
     * @param int $start 修剪的开始位置，取值为从0开始的一个索引值，修剪后的数组包含该位置的元素
     * @param int $stop 修剪的结束位置，取值为从0开始的一个索引值，修剪后的数组包含该位置的元素
     * @return int 返回操作完成后数组的长度。
     */
    public function arrTrim(string $key, string $path, int $start, int $stop): int {
        $this->checkNotEmpty($key);
        $this->checkNotEmpty($path);

        return $this->then(function (string $key, string $path, int $start, int $stop) {
            return $this->client->rawCommand('JSON.ARRTRIM', $key, $path, $start, $stop);
        }, [$key, $path, $start, $stop], function () use ($key, $path, $start, $stop) {
            return sprintf('JSON.ARRTRIM %s %s %u %u', $key, $path, $start, $stop);
        });
    }
}